﻿using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.Windows.Forms;

using Spencen.Mobile.UI.Animations;
using Spencen.Mobile.UI.Easings;
using Spencen.Mobile.UI.Primitives;
using Spencen.Mobile.UI.Controls;

using View = Spencen.Mobile.UI.Controls.View;

namespace Spencen.Mobile.UI
{
    public class Canvas : UserControl, IDrawingHost
    {
        private Bitmap _offBitmap;
        private Graphics _offGraphics;
        private AnimationManager _animationManager;
        private DrawingElement _selectedElement;
        private Dpi _dpi;
        private MenuBar _menu;

        private bool useClipping = true;

        public Canvas()
        {
            TargetDpi = new Dpi( 192, 192 );
            View = new Spencen.Mobile.UI.Controls.View( this ); // Create a default view automatically
            View.Size = new Size( ClientRectangle.Width, ClientRectangle.Height );
        }

        public event EventHandler<SelectedElementEventArgs> ElementSelected;

        [DefaultValue( null )]
        public Spencen.Mobile.UI.Controls.View View { get; set; }

        public Dpi Dpi
        {
            get
            {
                if ( _dpi.X == 0 )
                    _dpi = DrawingSupport.GetDpi( this );

                return _dpi;
            }
        }

        public Dpi TargetDpi { get; private set; }

        public MenuBar Menu
        {
            get { return _menu; }
            set
            {
                _menu = value;
                ( (Form)Parent ).Menu = value;
            }
        }

        protected override void OnPaintBackground( PaintEventArgs e )
        {
            // Do nothing - all drawing done in a single pass (OnPaint)
        }

        protected override void OnPaint( PaintEventArgs e )
        {
            //LogMessage( string.Format( "Starting Canvas OnPaint... ( X={0}, Y={1}, Width={2}, Height={2})", e.ClipRectangle.X, e.ClipRectangle.Y, e.ClipRectangle.Width, e.ClipRectangle.Height ) );

            if ( _offBitmap == null )
            {
                _offBitmap = new Bitmap( Width, Height );
                _offGraphics = Graphics.FromImage( _offBitmap );
            }

            if ( useClipping )
            {
                _offGraphics.Clip = new Region( e.ClipRectangle );
                _offGraphics.Clear( BackColor );
            }
            else
                _offGraphics.Clear( BackColor );

            var context = new GraphicsContext( _offGraphics, View );
            View.Render( context );

            // Draw the offscreen bitmap
            e.Graphics.DrawImage( _offBitmap, e.ClipRectangle, e.ClipRectangle, GraphicsUnit.Pixel );

            //LogMessage( "Completed Canvas OnPaint..." );
        }

        #region Transaction between Views - TODO replace with configurable transition as applied to any element

        public void SwitchFromLeft( View newView, TimeSpan duration, IEasingFunction function )
        {
            Switch( newView, duration, function, new Point( ClientRectangle.Width, 0 ) );
        }

        public void SwitchFromRight( View newView, TimeSpan duration, IEasingFunction function )
        {
            Switch( newView, duration, function, new Point( -ClientRectangle.Width, 0 ) );
        }

        public void Fade( View newView, TimeSpan duration, IEasingFunction function )
        {

        }

        protected void Switch( View newView, TimeSpan duration, IEasingFunction function, Point offset )
        {
            // Create an offscreen bitmap for the composite of the two views as they animate.
            var compositeBitmap = new Bitmap( ClientRectangle.Width, ClientRectangle.Height );

            // Create an offscreen bitmap to render the new view to.
            var newViewBitmap = new Bitmap( ClientRectangle.Width, ClientRectangle.Height );
            var newViewGraphics = Graphics.FromImage( newViewBitmap );
            newViewGraphics.Clear( BackColor );

            // Render the new view so to the offscreen bitmap so we can copy to the new position each frame
            var newContext = new GraphicsContext( newViewGraphics, newView );
            newView.Render( newContext );

            using ( Graphics canvasGraphics = CreateGraphics(), compositeGraphics = Graphics.FromImage( compositeBitmap ) )
            {
                bool done = false;
                int frame = 0;
                long startTick = Environment.TickCount;
                Point startOffset = Point.Empty;
                Brush backBrush = new SolidBrush( BackColor );

                while ( !done )
                {
                    frame++;
                    float percentageComplete = Math.Min( ( Environment.TickCount - startTick ) / (float)( duration.Ticks / 10000 ), 1.0f ); // Divide by 10000 to convert DateTime Ticks (100 nano-seconds) to milliseconds
                    float adjustedValue;

                    if ( function == null )
                        adjustedValue = percentageComplete;
                    else
                        adjustedValue = function.EasedValue( percentageComplete );

                    Point currentOffset = new Point( startOffset.X + (int)( offset.X * adjustedValue ), startOffset.Y + (int)( offset.Y * adjustedValue ) );
                    done = percentageComplete == 1.0f;

                    // Paint the "overrun" that may occur when using certain types of easing - e.g. elastic, bounce.
                    if ( offset.X < 0 && currentOffset.X < offset.X )
                        compositeGraphics.FillRectangle( backBrush,
                            currentOffset.X + Width + Width, 0,
                            -currentOffset.X - Width, Height );
                    else if ( offset.X > 0 && currentOffset.X > offset.X )
                        compositeGraphics.FillRectangle( backBrush,
                            0, 0,
                            currentOffset.X - Width, Height );

                    // Place the old view on the composite at the new offset position.
                    compositeGraphics.DrawImage( _offBitmap, currentOffset.X, currentOffset.Y );
                    // Place the new view on the composite either to the left or right of the old view.
                    compositeGraphics.DrawImage( newViewBitmap, 
                        offset.X < 0 ? currentOffset.X + Width : currentOffset.X - Width, 
                        currentOffset.Y );

                    // Render the composite to the on-screen surface.
                    canvasGraphics.DrawImage( compositeBitmap, 0, 0 );
                }
                backBrush.Dispose();
            }

            _offBitmap.Dispose(); // Drop the previous offscreen bitmap
            _offBitmap = newViewBitmap; // Replace with the new one.

            _offGraphics.Dispose();
            _offGraphics = newViewGraphics;
            
            compositeBitmap.Dispose();

            if ( View != newView )
            {
                View.Children.Clear();
                View = newView;
            }

            Refresh(); // Shouldn't need this but am getting dodgy first render on emulator so use this to force refresh after switch.
        }

        #endregion

        #region User Interaction Events

        protected override void OnMouseDown( MouseEventArgs e )
        {
            base.OnMouseDown( e );

            var element = View.Children.HitTest( new Point( e.X, e.Y ), new GraphicsContext( (Graphics) null, View ) );
            _selectedElement = element;
            if ( element != null )
            {
                element.OnMouseDown( e );
                OnSelectedElement( new SelectedElementEventArgs( element ) );
            }
        }

        protected override void OnMouseUp( MouseEventArgs e )
        {
            base.OnMouseUp( e );

            var element = View.Children.HitTest( new Point( e.X, e.Y ), new GraphicsContext( ( Graphics) null, View ) );
            if ( element != null )
            {
                _selectedElement = null;
                element.OnMouseUp( e );
            }
        }

        protected override void OnMouseMove( MouseEventArgs e )
        {
            base.OnMouseMove( e );

            if ( _selectedElement != null )
            {
                _selectedElement.OnMouseMove( e );
            }
        }

        protected virtual void OnSelectedElement( SelectedElementEventArgs e )
        {
            var handler = ElementSelected;
            if ( handler != null )
                handler( this, e );
        }

        protected override void OnKeyPress( KeyPressEventArgs e )
        {
            base.OnKeyPress( e );
        }

        protected override void OnKeyDown( KeyEventArgs e )
        {
            base.OnKeyDown( e );

            if ( View != null )
                View.ProcessKeyDown( e );
        }

        #endregion

         #region IDrawingHost Members

        public AnimationManager AnimationManager
        {
            get 
            {
                if ( _animationManager == null )
                    _animationManager = new AnimationManager( this );
                return _animationManager; 
            }
        }

        public void Invalidate( DrawingElement element )
        {
            var clipRectangle = GetBounds( element );
            clipRectangle.Inflate( 1, 1 ); // TODO: Need to correct elments that are drawing 1 pixel outside their bounds
            Invalidate( clipRectangle );

            //Debug.WriteLine( string.Format( "Invalidate region for {4}: ( X={0}, Y={1}, Width={2}, Height={2})", clipRectangle.X, clipRectangle.Y, clipRectangle.Width, clipRectangle.Height, element.GetType().Name ) );
        }

        private Rectangle GetBounds( DrawingElement element )
        {
            return element.TransformedBounds( new GraphicsContext( element ) );
        }

        #endregion

        [Conditional( "DEBUG" )]
        private void LogMessage( string message )
        {
            Debug.WriteLine( message );
        }
    }
}
